//
//  WCService.swift
//  RemoteService
//
//  Created by Longxiang Guo (QXQ4633) on 2021/5/23.
//

import Foundation
import WatchConnectivity
import Combine

open class WatchMessage {
    public let replyHandler: (([String: Any]) -> Void)?
    open var message: Dictionary<String, Any>

    init(message: Dictionary<String, Any>, withReply reply: (([String: Any]) -> Void)? = nil) {
        self.message = message
        self.replyHandler = reply
    }
}

extension WatchMessage: CustomStringConvertible, CustomDebugStringConvertible {
    public var description: String {
        return debugDescription
    }

    public var debugDescription: String {
        return message.debugDescription
    }
}

public struct WatchFile {
    let url: URL
    let metadata: [String: Any]?
}

protocol WCServiceProtocol {
    
    var receiveMessage: AnyPublisher<WatchMessage,Error>                 { get }
    var sendMessage: PassthroughSubject<WatchMessage, Error>             { get }
    
    var receiveApplicatonContext: AnyPublisher<WatchMessage, Error>      { get }
    var sendApplicationContext: PassthroughSubject<WatchMessage, Error>  { get }
   
    var receiveUserInfo: AnyPublisher<WatchMessage, Error>               { get }
    var sendUserInfo: PassthroughSubject<WatchMessage, Error>            { get }
    
    var receiveFile: AnyPublisher<WatchFile, Error>                      { get }
    var sendFile: PassthroughSubject<WatchFile, Error>                   { get }
    
    var watchSessionIsReachable: AnyPublisher<Bool,Never>                { get }
    func startSession()
}

open class WCService: WCServiceProtocol, WCSessionWrapperDelegate {

    private var sessionCancellable = Set<AnyCancellable>()
    private var messageCancellable = Set<AnyCancellable>()
    private var cancellable = Set<AnyCancellable>()



    private let isReachable = CurrentValueSubject<Bool,Never>(false)
    
    private let receiveMessageQueue = PassthroughSubject<WatchMessage,Error>()

    private let sendMessageObserver = PassthroughSubject<WatchMessage,Error>()
    private var sendMessageQueue = PassthroughSubject<WatchMessage, Error>()

    private let receiveApplicatonContextQueue = PassthroughSubject<WatchMessage, Error>()
    private var sendApplicationContextQueue = PassthroughSubject<WatchMessage, Error>()
    
    private let receiveUserInfoQueue = PassthroughSubject<WatchMessage, Error>()
    private var sendUserInfoQueue = PassthroughSubject<WatchMessage, Error>()
    
    private var receiveFileQueue = PassthroughSubject<WatchFile, Error>()
    private var sendFileQueue = PassthroughSubject<WatchFile, Error>()

    private var applicationContext: Dictionary<String, Any>
    
    open var session: WCSessionWrapperProtocol
    // MARK: - WCServiceProtocol Implementation

    open var receiveMessage: AnyPublisher<WatchMessage, Error> {
        return receiveMessageQueue.eraseToAnyPublisher()
    }

    open var sendMessage: PassthroughSubject<WatchMessage, Error> {
        return sendMessageObserver
    }

    open var receiveApplicatonContext: AnyPublisher<WatchMessage, Error> {
        return receiveApplicatonContextQueue.eraseToAnyPublisher()
    }

    open var sendApplicationContext: PassthroughSubject<WatchMessage,Error> {
        return sendApplicationContextQueue
    }

    open var receiveUserInfo: AnyPublisher<WatchMessage, Error> {
        return receiveUserInfoQueue.eraseToAnyPublisher()
    }

    open var sendUserInfo: PassthroughSubject<WatchMessage,Error> {
        return sendUserInfoQueue
    }

    open var receiveFile: AnyPublisher<WatchFile,Error> {
        return receiveFileQueue.eraseToAnyPublisher()
    }

    open var sendFile: PassthroughSubject<WatchFile,Error> {
        return sendFileQueue
    }

    open var watchSessionIsReachable: AnyPublisher<Bool,Never> {
        return isReachable.eraseToAnyPublisher()
    }

   required public init(session: WCSessionWrapperProtocol) {
        self.session = session
        applicationContext = Dictionary<String, Any>()

        startMessageObserver()
    }

    open func startSession() {
        self.session.delegate = self
        self.session.activateSession()
    }

    private func startSending() {
        self.isReachable.value = self.session.reachable
        sendApplicationContextQueue.sink { (_) in
         
        } receiveValue: { [unowned self] ac in
            // ApplicationContext is only updated, when context data changes. This can result in not sending information from the phone to watch app upon startup. One workaround is to add a UUID to the application context dictionary in order to force sending the context (https://forums.developer.apple.com/thread/46107)
            ac.message["UUID"] = UUID().uuidString as Any?
            _ = try? self.session.updateApplicationContext(ac.message)
        }.store(in: &sessionCancellable)

        sendUserInfoQueue.sink { (_) in
            
        }
        receiveValue: { [unowned self] ui in
            _ = self.session.transferUserInfo(ui.message)
        }.store(in: &sessionCancellable)

        sendFileQueue.sink { (_) in
             
        } receiveValue: { [unowned self] file in
            _ = self.session.transferFile(file.url, metadata: file.metadata)
        }.store(in: &sessionCancellable)

        if self.session.reachable {
            startSendingMessages()
        }
    }

    private func startSendingMessages() {
        sendMessageQueue.sink { (_) in
            
        } receiveValue: {[unowned self] msg in
            self.session.sendMessage(
                msg.message,
                replyHandler: msg.replyHandler,
                errorHandler: { err in
                    print("ERROR sending msg (\(msg.message): \(err)")
                }
            )
        }.store(in: &messageCancellable)
    }

    private func stopSendingMessages() {
        messageCancellable = Set<AnyCancellable>()
        sendMessageQueue = PassthroughSubject<WatchMessage, Error>()
    }

    private func startMessageObserver() {
        sendMessageObserver
            .flatMap({ [unowned self] (msg) -> AnyPublisher<WatchMessage, Timer.TimerPublisher.Failure> in
                // Every time someone tries to send a message it is checked whether the session is
                // ready to send messages. If a change is detected thorugh this check the sendMessageQueue
                // will be activated/deactivated accordingly.
                // If the other side is reachable the message will be forwarded to the sendMessageQueue
                // which is active then and sends the message.
                // If it's not reachable after even several tries the message will be queued for a later
                // delivery when reachability is reported to be true again.
                // We need to do this recahability check because there are issues in watchOS 3.1.3 (and maybe later)
                // where the WatchConnectivity framework does not call sessionReachabilityDidChange for
                // every reachability change. The watch app might therefore still think that the phone
                // is not reachable although it is.
                return self.ensurePhoneReachability(trys: 3).map { _ in return msg }.eraseToAnyPublisher()
                
            }).sink { (_) in
                
            } receiveValue: { (msg) in
                self.sendMessageQueue.send(msg)
            }.store(in: &cancellable)
    }

    private func ensurePhoneReachability(trys remainingTrys: Int) -> AnyPublisher<Void, Timer.TimerPublisher.Failure> {
        checkForUnexpectedReachabilityChange()

        if isReachable.value || remainingTrys <= 0 {
            return Just<Void>.init(()).eraseToAnyPublisher()
        } else {
            print("Try to get other side reachable again in a moment. Remaining trys: \(remainingTrys)")
            
         return Timer.publish(every: 0.5, on: RunLoop.main, in: RunLoop.Mode.default).flatMap { _ in
             return self.ensurePhoneReachability(trys: remainingTrys - 1)
         }.eraseToAnyPublisher()
        }
    }

    private func checkForUnexpectedReachabilityChange() {
        if session.reachable != isReachable.value {
            print("Detected unexpected reachability change! Notifying WCSessionDelegate now. (Expected:\(isReachable.value) - Actual: \(session.reachable))")
            sessionReachabilityDidChange(session)
        } else {
            print("Detected no reachability change - value is still: \(isReachable.value)")
        }
    }

    // MARK: - WCSessionWrapperDelegate Implementation

    open func session(_ session: WCSessionWrapperProtocol, activationDidCompleteWith activationState: WCSessionActivationState, error: Error?) {
        print("watch session activation state changed: \(activationState) - error: \(String(describing: error))")
        if activationState == .activated {
            startSending()
        } else {
            stopSendingMessages()
            sessionCancellable = Set<AnyCancellable>()
        }
    }

    open func session(_ session: WCSessionWrapperProtocol, didReceiveMessage message: [String: Any], replyHandler: @escaping ([String: Any]) -> Void) {
        let watchMessage = WatchMessage(message: message, withReply: replyHandler)
        receiveMessageQueue.send(watchMessage)
    }

    open func session(_ session: WCSessionWrapperProtocol, didReceiveMessage message: [String: Any]) {
        let watchMessage = WatchMessage(message: message, withReply: nil)
        receiveMessageQueue.send(watchMessage)
    }

    open func session(_ session: WCSessionWrapperProtocol, didReceiveApplicationContext applicationContext: [String: Any]) {
        self.applicationContext = applicationContext
        let updatedApplicationContext = WatchMessage(message: applicationContext)
        receiveApplicatonContextQueue.send(updatedApplicationContext)
    }

    open func session(_ session: WCSessionWrapperProtocol, didReceiveUserInfo userInfo: [String: Any]) {
        let userInfo = WatchMessage(message: userInfo)
        receiveUserInfoQueue.send(userInfo)
    }

    open func session(_ session: WCSessionWrapperProtocol, didReceiveFile file: WCSessionFile) {
        let file = WatchFile.init(url: file.fileURL, metadata: file.metadata)
        receiveFileQueue.send(file)
    }

    open func sessionReachabilityDidChange(_ session: WCSessionWrapperProtocol) {
        isReachable.value = session.reachable
        if isReachable.value {
            startSendingMessages()
        } else {
            stopSendingMessages()
        }
    }

}
